import java.io.*;
import java.util.Arrays;

public class ex1Main {

    // Scale using forward sampling
    public static ImageAsArray.ImageAsArrayHolder ScaleForward(ImageAsArray.ImageAsArrayHolder img, float factor) {
	// Calulate the new image size
	int newWidth = Math.round(img.width * factor);
	int newHeight = Math.round(img.height * factor);

	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(newWidth, newHeight);

	for (int ix = 0; ix < img.width; ix++) {
	    for (int iy = 0; iy < img.height; iy++) {
		for (int dx = (int) (ix * factor); dx < (int) (ix * factor) + factor; dx++) {
		    for (int dy = (int) (iy * factor); dy < (int) (iy * factor) + factor; dy++) {
			for (int k = 0; k < 4; k++) {
			    newImg.pixels_ARGB[(dy * newImg.width + dx) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
			}
		    }
		}
	    }
	}

	return newImg;
    }

    // General scale method, gets a backward sampling algorithm as parameter
    public static ImageAsArray.ImageAsArrayHolder Scale(ImageAsArray.ImageAsArrayHolder img, float factor, Sampling resampler) {
	// Calculate the new image size
	int newWidth = Math.round(img.width * factor);
	int newHeight = Math.round(img.height * factor);
	int[] sample = { 0, 0, 0, 0 };

	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(newWidth, newHeight);

	// Sample and save the results
	for (int ix = 0; ix < newImg.width; ix++) {
	    for (int iy = 0; iy < newImg.height; iy++) {
		sample = resampler.getSample(img, ix / factor, iy / factor, factor);

		for (int k = 0; k < 4; k++) {
		    newImg.pixels_ARGB[(iy * newImg.width + ix) * 4 + k] = sample[k];
		}
	    }
	}

	return newImg;
    }

    // Rotation using forwards sampling
    public static ImageAsArray.ImageAsArrayHolder RotateForward(ImageAsArray.ImageAsArrayHolder img, float angle) {
	float degree = angle * ((float) Math.PI / 180);

	// Calculate the new image edges
	int[] newPoints = { 0, 0, img.width - 1, 0, 0, img.height - 1, img.width - 1, img.height - 1 };
	int tempx, tempy;
	for (int i = 0; i < 8; i = i + 2) {
	    tempx = (int) Math.round(Math.cos(degree) * newPoints[i] + (-1 * Math.sin(degree) * newPoints[i + 1]));
	    tempy = (int) Math.round(Math.sin(degree) * newPoints[i] + Math.cos(degree) * newPoints[i + 1]);
	    newPoints[i] = tempx;
	    newPoints[i + 1] = tempy;
	}

	// Calculate the new image's necessary width & height
	int newWidth = Math.max(Math.abs(newPoints[0] - newPoints[6]), Math.abs(newPoints[2] - newPoints[4])) + 1;
	int newHeight = Math.max(Math.abs(newPoints[1] - newPoints[7]), Math.abs(newPoints[3] - newPoints[5])) + 1;
	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(newWidth, newHeight);

	// Calculate offset to the original axis
	float offsetx = Math.abs(Math.min(Math.min(newPoints[0], newPoints[6]), Math.min(newPoints[2], newPoints[4])));
	float offsety = Math.abs(Math.min(Math.min(newPoints[1], newPoints[7]), Math.min(newPoints[3], newPoints[5])));

	float dx, dy;
	Arrays.fill(newImg.pixels_ARGB, 255);
	for (int iy = 0; iy < img.height; iy++) {
	    for (int ix = 0; ix < img.width; ix++) {
		dx = (float) (Math.cos(degree) * (ix) + -1 * Math.sin(degree) * (iy) + offsetx);
		dy = (float) (Math.sin(degree) * (ix) + Math.cos(degree) * (iy) + offsety);

		// Handling cases where sin & cos calculations are almost integers (and they should be)
		dx = (dx - Math.round(dx)) < Math.pow(10, -6) ? Math.round(dx) : dx;
		dy = (dy - Math.round(dy)) < Math.pow(10, -6) ? Math.round(dy) : dy;

		int ceilDx = (int) Math.min(Math.floor(dx + 1), img.width);
		int floorDx = (int) Math.max(Math.floor(dx), 0);
		int ceilDy = (int) Math.min(Math.floor(dy + 1), img.height);
		int floorDy = (int) Math.max(Math.floor(dy), 0);

		for (int k = 0; k < 4; k++) {
		    newImg.pixels_ARGB[((ceilDy * newImg.width) + ceilDx) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((floorDy * newImg.width) + floorDx - 1) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((floorDy * newImg.width) + floorDx + 1) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((ceilDy * newImg.width) + ceilDx - 1) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((ceilDy * newImg.width) + ceilDx + 1) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((floorDy * newImg.width) + floorDx - 2) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((floorDy * newImg.width) + floorDx + 2) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((ceilDy * newImg.width) + ceilDx - 2) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		    newImg.pixels_ARGB[((ceilDy * newImg.width) + ceilDx + 2) * 4 + k] = img.pixels_ARGB[(iy * img.width + ix) * 4 + k];
		}
	    }
	}

	return newImg;
    }

    // General rotation method, gets a backward sampling algorithm as parameter
    public static ImageAsArray.ImageAsArrayHolder Rotate(ImageAsArray.ImageAsArrayHolder img, float angle, Sampling resampler) {
	float degree = angle * ((float) Math.PI / 180);

	// Calculate the new image edges
	int[] newPoints = { 0, 0, img.width - 1, 0, 0, img.height - 1, img.width - 1, img.height - 1 };
	int tempx, tempy;
	for (int i = 0; i < 8; i = i + 2) {
	    tempx = (int) Math.round(Math.cos(degree) * newPoints[i] + (-1 * Math.sin(degree) * newPoints[i + 1]));
	    tempy = (int) Math.round(Math.sin(degree) * newPoints[i] + Math.cos(degree) * newPoints[i + 1]);
	    newPoints[i] = tempx;
	    newPoints[i + 1] = tempy;
	}

	// Calculate the new image's necessary width & height
	int newWidth = Math.max(Math.abs(newPoints[0] - newPoints[6]), Math.abs(newPoints[2] - newPoints[4])) + 1;
	int newHeight = Math.max(Math.abs(newPoints[1] - newPoints[7]), Math.abs(newPoints[3] - newPoints[5])) + 1;
	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(newWidth, newHeight);

	// Calculate offset to the original axis
	float offsetx = Math.abs(Math.min(Math.min(newPoints[0], newPoints[6]), Math.min(newPoints[2], newPoints[4])));
	float offsety = Math.abs(Math.min(Math.min(newPoints[1], newPoints[7]), Math.min(newPoints[3], newPoints[5])));

	int[] sample = { 0, 0, 0, 0 };
	float origx, origy;
	for (int ix = 0; ix < newImg.width; ix++) {
	    for (int iy = 0; iy < newImg.height; iy++) {
		origx = (float) (Math.cos(degree) * (ix - offsetx) + Math.sin(degree) * (iy - offsety));
		origy = (float) (-1 * Math.sin(degree) * (ix - offsetx) + Math.cos(degree) * (iy - offsety));

		// Handling cases where sin & cos calculations are almost integers (and they should be)
		origx = (origx - Math.round(origx)) < Math.pow(10, -6) ? Math.round(origx) : origx;
		origy = (origy - Math.round(origy)) < Math.pow(10, -6) ? Math.round(origy) : origy;

		if (origx > img.width - 1 || origx < 0 || origy < 0 || origy > img.height - 1) {
		    Arrays.fill(sample, 255);
		} else {
		    sample = resampler.getSample(img, origx, origy, 1);
		}

		for (int k = 0; k < 4; k++) {
		    newImg.pixels_ARGB[(iy * newImg.width + ix) * 4 + k] = sample[k];
		}
	    }
	}

	return newImg;
    }

    // Dithering helper function which finds the nearest black / white color to a given RGB one
    private static int[] findClosestColor(int[] aRGB) {
	int[] newColor = { 0, 0, 0, 0 };
	float sum = 0;

	for (int i = 1; i < aRGB.length; i++) {
	    sum += aRGB[i];
	}
	if (sum / 3 > 255 / 2) {
	    Arrays.fill(newColor, 255);
	} else {
	    Arrays.fill(newColor, 0);
	}
	newColor[0] = 255;

	return newColor;
    }

    // Floyd-Steinberg dithering algorithm
    public static ImageAsArray.ImageAsArrayHolder Dithering(ImageAsArray.ImageAsArrayHolder img) {

	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(img.width, img.height);
	for (int i = 0; i < img.pixels_ARGB.length; i++) {
	    newImg.pixels_ARGB[i] = img.pixels_ARGB[i];
	}

	int[] oldPixel = { 0, 0, 0, 0 };
	int[] newPixel = { 0, 0, 0, 0 };
	int[] error = { 0, 0, 0, 0 };

	for (int iy = 0; iy < newImg.height; iy++) {
	    for (int ix = 0; ix < newImg.width; ix++) {
		// Save the old pixel, so we could calulate the error later on
		for (int i = 0; i < 4; i++) {
		    oldPixel[i] = newImg.pixels_ARGB[(iy * newImg.width + ix) * 4 + i];
		}

		// Find the closest black/white color
		newPixel = findClosestColor(oldPixel);

		// Apply the new pixel color to the image, and calculate the error
		for (int k = 0; k < 4; k++) {
		    newImg.pixels_ARGB[(iy * img.width + ix) * 4 + k] = newPixel[k];
		    error[k] = oldPixel[k] - newPixel[k];
		}

		// Spread the errors over the pixels nearby
		for (int k = 0; k < 4; k++) {
		    if (ix != newImg.width - 1) {
			newImg.pixels_ARGB[(iy * newImg.width + ix + 1) * 4 + k] += (int) ((7.0 / 16.0) * error[k]);
		    }
		    if (iy != newImg.height - 1) {
			if (ix != 0) {
			    newImg.pixels_ARGB[((iy + 1) * newImg.width + (ix - 1)) * 4 + k] += (int) ((3.0 / 16.0) * error[k]);
			}
			newImg.pixels_ARGB[((iy + 1) * newImg.width + ix) * 4 + k] += (int) ((5.0 / 16.0) * error[k]);
			if (ix != newImg.width - 1) {
			    newImg.pixels_ARGB[((iy + 1) * newImg.width + (ix + 1)) * 4 + k] += (int) ((1.0 / 16.0) * error[k]);
			}
		    }
		}
	    }
	}

	return newImg;

    }

    // Convolution method which gets a custom matrix
    private static ImageAsArray.ImageAsArrayHolder Convolution(ImageAsArray.ImageAsArrayHolder img, float[] params) {
	assert (params.length == 9);

	float[] sum = { 0, 0, 0, 0 };
	int curPx = 0;
	int curPy = 0;

	ImageAsArray.ImageAsArrayHolder newImg = new ImageAsArray.ImageAsArrayHolder(img.width, img.height);

	for (int iy = 0; iy < newImg.height; iy++) {
	    for (int ix = 0; ix < newImg.width; ix++) {
		Arrays.fill(sum, 0);
		for (int i = -1; i < 2; i++) {
		    for (int j = -1; j < 2; j++) {

			if ((iy + i) < 0) {
			    curPy = 0;
			} else if ((iy + i) > newImg.height - 1) {
			    curPy = newImg.height - 1;
			} else {
			    curPy = iy + i;
			}

			if ((ix + j) < 0) {
			    curPx = 0;
			} else if ((ix + j) > newImg.width - 1) {
			    curPx = newImg.width - 1;
			} else {
			    curPx = ix + j;
			}

			for (int k = 0; k < 4; k++) {
			    sum[k] += img.pixels_ARGB[(curPy * img.width + curPx) * 4 + k] * params[(i + 1) * 3 + (j + 1)];
			}
		    }
		}

		for (int k = 0; k < 4; k++) {
		    if (sum[k] > 255) {
			sum[k] = 255;
		    }
		    if (sum[k] < 0) {
			sum[k] = 0;
		    }
		    newImg.pixels_ARGB[(iy * newImg.width + ix) * 4 + k] = (int) sum[k];
		}
	    }
	}
	return newImg;
    }

    public static void main(String[] args) {
	String srcImgFilename = null, dstImgFilename = null, algoName = null, algoParams = null;
	ImageAsArray.ImageAsArrayHolder srcImg = null, dstImg = null;
	float scaleFactor, rotateAngle;

	// Read configuration file
	assert args.length == 1;
	try {
	    FileReader confFile = new FileReader(args[0]);
	    BufferedReader bufRead = new BufferedReader(confFile);

	    srcImgFilename = bufRead.readLine();
	    dstImgFilename = bufRead.readLine();
	    algoName = bufRead.readLine();
	    algoParams = bufRead.readLine();

	    bufRead.close();
	} catch (Exception e) {
	    System.out.println("Error reading configuration file: " + e.getMessage());
	    return;
	}

	// Load the source image
	try {
	    srcImg = ImageAsArray.LoadImageAsByteArrayARGB(srcImgFilename);
	} catch (Exception e) {
	    System.out.println("Failed loading image file: " + e.getMessage());
	    return;
	}

	// Run the requested algorithm
	if (algoName.equalsIgnoreCase("Scale_forward")) {
	    scaleFactor = Float.parseFloat(algoParams);
	    dstImg = ScaleForward(srcImg, scaleFactor);
	} else if (algoName.equalsIgnoreCase("Scale_nearestneighbor")) {
	    scaleFactor = Float.parseFloat(algoParams);
	    dstImg = Scale(srcImg, scaleFactor, new NearestNeighbor());
	} else if (algoName.equalsIgnoreCase("Scale_bilinear")) {
	    scaleFactor = Float.parseFloat(algoParams);
	    dstImg = Scale(srcImg, scaleFactor, new Bilinear());
	} else if (algoName.equalsIgnoreCase("Scale_gaussian")) {
	    scaleFactor = Float.parseFloat(algoParams);
	    dstImg = Scale(srcImg, scaleFactor, new Gaussian());
	} else if (algoName.equalsIgnoreCase("Rotate_forward")) {
	    rotateAngle = Float.parseFloat(algoParams);
	    dstImg = RotateForward(srcImg, rotateAngle);
	} else if (algoName.equalsIgnoreCase("Rotate_nearestneighbor")) {
	    rotateAngle = Float.parseFloat(algoParams);
	    dstImg = Rotate(srcImg, rotateAngle, new NearestNeighbor());
	} else if (algoName.equalsIgnoreCase("Rotate_bilinear")) {
	    rotateAngle = Float.parseFloat(algoParams);
	    dstImg = Rotate(srcImg, rotateAngle, new Bilinear());
	} else if (algoName.equalsIgnoreCase("Rotate_gaussian")) {
	    rotateAngle = Float.parseFloat(algoParams);
	    dstImg = Rotate(srcImg, rotateAngle, new Gaussian());
	} else if (algoName.equalsIgnoreCase("Blur_gaussian")) {
	    float[] convFactors = { (float) 1.0 / 16, (float) 2.0 / 16, (float) 1.0 / 16, (float) 2.0 / 16, (float) 4.0 / 16, (float) 2.0 / 16, (float) 1.0 / 16, (float) 2.0 / 16,
		    (float) 1.0 / 16 };
	    dstImg = Convolution(srcImg, convFactors);
	} else if (algoName.equalsIgnoreCase("Blur_uniform")) {
	    float[] convFactors = { (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9, (float) 1.0 / 9,
		    (float) 1.0 / 9 };
	    dstImg = Convolution(srcImg, convFactors);
	} else if (algoName.equalsIgnoreCase("Edge_detect")) {
	    float[] convFactors = { (float) -1, (float) -1, (float) -1, (float) -1, (float) 8, (float) -1, (float) -1, (float) -1, (float) -1 };
	    dstImg = Convolution(srcImg, convFactors);
	} else if (algoName.equalsIgnoreCase("Convolution_custom")) {
	    String[] convFactorsString = algoParams.split(",");
	    float[] convFactors = new float[convFactorsString.length];
	    for (int i = 0; i < convFactorsString.length; i++) {
		convFactors[i] = Float.parseFloat(convFactorsString[i]);
	    }
	    dstImg = Convolution(srcImg, convFactors);
	} else if (algoName.equalsIgnoreCase("Dithering")) {
	    dstImg = Dithering(srcImg);
	} else {
	    System.out.println(algoName + ": No such algorithm exists.");
	    return;
	}

	// Save the destination image
	try {
	    ImageAsArray.SaveImageFromByteArrayARGB(dstImg, dstImgFilename);
	} catch (Exception e) {
	    System.out.println("Failed saving image file: " + e.getMessage());
	    return;
	}
    }
}
